package org.activequant.util.charting;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;

import javax.swing.text.NumberFormatter;

import org.activequant.core.types.TimeStamp;
import org.activequant.util.pattern.events.Event;
import org.activequant.util.pattern.events.IEventListener;
/**
 * Holds the following associated variables:
 * <ul>
 * <li>newColumnEvent(Event&lt;Column&gt;)</li>
 * <li>newBoxEvent(Event&lt;Box&gt;)</li>
 * <li>currentTimeStamp(TimeStamp)</li>
 * <li>internalBoxListener(IEventListener&lt;Box&gt;)</li>
 * <li>currentReversalPoint(double)</li>
 * <li>columns(List&lt;Column&gt;)</li>
 * <li>currentColumn(Column)</li>
 * <li>currentBox(Box)</li>
 * <li>base(double)</li>
 * <li>boxSize(double)</li>
 * <li>reversalSize(int)</li>
 * </ul>
 * @author Dimitar
 *
 */
public class PointAndFigureChart {
	/**
	 * private Event&lt;Column&gt; newColumnEvent = new Event&lt;Column&gt;();
	 */
	private Event<Column> newColumnEvent = new Event<Column>();
	/**
	 * private Event&lt;Box&gt; newBoxEvent = new Event&lt;Box&gt;();
	 */
	private Event<Box> newBoxEvent = new Event<Box>();
	/**
	 * private TimeStamp currentTimeStamp = null;
	 */
	private TimeStamp currentTimeStamp = null;
	/**
	 * private IEventListener&lt;Box&gt; internalBoxListener = new IEventListener&lt;Box&gt;(){...}<br/>
	 * A listener whose job is to set the timestamp(TimeStamp) of the given event(Box) with the associated currentTimeStamp(TimeStamp) (if it is not null)
	 */
	private IEventListener<Box> internalBoxListener = new IEventListener<Box>(){
		public void eventFired(Box event) {
			if(currentTimeStamp!=null){
				event.timestamp = currentTimeStamp;
			}
		}		
	};
	/**
	 * Constructs a PointAndFigureChart and registers its internalBoxListener(IEventListener&lt;Box&gt;) to listen for events from the associated newBoxEvent(Event&lt;Box&gt;)
	 */
	public PointAndFigureChart() {
		newBoxEvent.addEventListener(internalBoxListener);
	}
	
	/**
	 * An enum. Has these 2 values defined:<br/>
	 * X,O
	 * @author Dimitar
	 *
	 */
	public enum Type{
		X,O;
	}
	/**
	 * A nested class.<br/>
	 * Holds the following associated variables:
	 * <ul>
	 * <li>boxes(List&lt;Box&gt;)</li>
	 * </ul>
	 * @author Dimitar
	 *
	 */
	public class Column{
		/**
		 * private List&lt;Box&gt; boxes = new ArrayList&lt;Box&gt();
		 */
		private List<Box> boxes = new ArrayList<Box>();
		/**
		 * Adds the given b(Box) to the associated boxes(List&lt;Box&gt;)
		 * @param b
		 */
		void addBox(Box b){boxes.add(b);}
		/**
		 * returns the associated boxes(List&lt;Box&gt;)
		 * @return
		 */
		public List<Box> getBoxes() {
			return boxes;
		}
	}
	/**
	 * A nested class.<br/>
	 * Holds the following associated variables:
	 * <ul>
	 * <li>type(Type)</li>
	 * <li>bottomPrice(double)</li>
	 * <li>size(double)</li>
	 * <li>timestamp(TimeStamp)</li>
	 * </ul>
	 * @author Dimitar
	 *
	 */
	public class Box{
		/**
		 * public Type type;
		 */
		public Type type;
		/**
		 * public double bottomPrice;
		 */
		public double bottomPrice;
		/**
		 * public double size;
		 */
		public double size;
		/**
		 * public TimeStamp timestamp;
		 */
		public TimeStamp timestamp;
		/**
		 * Returns a String in the form:<br/>
		 * "Bottom=${bottomPrice}, size=${size}, type=${type}, ${timestamp}"
		 * , where timestamp is optional
		 */
		public String toString(){
			String ret = "Bottom="+bottomPrice+", size="+size+", type="+type;
			if(timestamp!=null)ret+=", "+timestamp.getDate();
			return ret; 
		}
	}
	
	public String toString(Column[] columns) {
		
		// find the minimum of all ... 
		double min = findMimimalValue(columns);
		double max = findMaximalValue(columns);
		double boxSize = columns[0].boxes.get(0).size;
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		int m = "yyyy-MM-dd HH:mm:ss".length();
		// 
		int height = (int)((max-min)/boxSize) + 1 + m;
		int width = columns.length;
		char[][] chart = new char[width][height];
		for(int y=0;y<height;y++){
			for(int x=0;x<width;x++){
				chart[x][y] = ' ';
			}
		}
		
		int xPos = 0; 
		for(Column c : columns){
			for(Box b : c.boxes){
				int p = (int)((b.bottomPrice  - min) / boxSize); 
				int yPos = height - p - 1 - m ; // (numberOfBoxes(b.bottomPrice,min,boxSize));
				chart[xPos][yPos] = b.type==Type.X?'X':'O';
			}
			// add the date.
			Box firstBox = c.getBoxes().get(0); 
			if(firstBox.timestamp!=null){
				String formattedDate = sdf.format(firstBox.timestamp.getDate());
				
				for(int index = 0; index < m;index++){
					chart[xPos][height - m + index] = formattedDate.charAt(index);
				}
			}			
			xPos++;			
		}
		NumberFormatter nf = new NumberFormatter();
		
		StringBuffer ret = new StringBuffer();
		for(int y=0;y<height;y++){
			
			try {
				ret.append(""+ nf.valueToString((max - (y) * boxSize))+"    \t\t\t");
			} catch (ParseException e) {
				throw new RuntimeException(e);
			} 
			
			for(int x=0;x<width;x++){
				ret.append(chart[x][y]);
			}
			ret.append("\n");
		}
		return ret.toString(); 
	}
	/**
	 * Iterates the boxes(List&lt;Box&gt;) of each of the given columns(Column[]) looking at the bottomPrice(double) of each Box and returns the minimal(double) value found
	 * @param columns
	 * @return
	 */
	private double findMimimalValue(Column[] columns){
		double min = Double.MAX_VALUE;
		for(Column c : columns){
			for(Box b : c.boxes ){
				if(b.bottomPrice<min)min=b.bottomPrice;
			}
		}
		return min; 
	}
	/**
	 * Iterates the boxes(List&lt;Box&gt;) of each of the given columns(Column[]) looking at the bottomPrice(double) of each Box and returns the maximal(double) value found
	 * @param columns
	 * @return
	 */
	private double findMaximalValue(Column[] columns){
		double max = Double.MIN_VALUE;
		for(Column c : columns){
			for(Box b : c.boxes ){
				if(b.bottomPrice>max)max=b.bottomPrice;
			}
		}
		return max; 
	}
	/**
	 * private double currentReversalPoint = 0;
	 */
	private double currentReversalPoint = 0; 
	/**
	 * private List&lt;Column&gt; columns = new ArrayList&lt;Column&gt;();
	 */
	private List<Column> columns = new ArrayList<Column>();
	/**
	 * private Column currentColumn;
	 */
	private Column currentColumn; 
	/**
	 * private Box currentBox; 
	 */
	private Box currentBox; 
	/**
	 * private double base;
	 */
	private double base; 
	/**
	 * private double boxSize;
	 */
	private double boxSize;
	/**
	 * private int reversalSize;
	 */
	private int reversalSize; 
	/**
	 * <strong>1.</strong> Sets the associated base(double), boxSize(double) and reversalSize(int) with the given base(double), boxSize(double) and reversalSize(int).<br/>
	 * <strong>2.</strong> Initializes the associated currentBox(Box) to a new Box whose type(Type)=Type.X, bottomPrice(double) and size(double) are set with the given
	 * base(double) and boxSize(double).<br/>
	 * <strong>3.</strong> Initializes the associated currentColumn(Column) to a new Column and adds the associated currentBox(Box) to its boxes(List&lt;Box&gt;)<br/>
	 * <strong>4.</strong> Adds the associated currentColumn(Column) to the associated columns(List&lt;Column&gt;)
	 * @param base
	 * @param boxSize
	 * @param reversalSize
	 */
	public void initialize(double base, double boxSize, int reversalSize){
		this.base = base; 
		this.boxSize = boxSize;
		this.reversalSize = reversalSize; 
		currentColumn = new Column();
		currentBox = new Box();
		currentBox.type = Type.X;
		currentBox.bottomPrice = base; 
		currentBox.size = boxSize; 
		currentColumn.addBox(currentBox);
		columns.add(currentColumn);
	}
	
	public void handlePrice(double price, TimeStamp timestamp) {
		currentTimeStamp = timestamp;
		handlePrice(price);
	}
	
	public void handlePrice(double price) {
		if(price>(currentBox.bottomPrice + (1*boxSize))){
			if(currentBox.type == Type.X){
				while(price>(currentBox.bottomPrice + (1*boxSize))){
					currentBox = new Box();
					currentBox.type = Type.X;
					base = base + boxSize;
					currentBox.bottomPrice = base;
					currentBox.size = boxSize; 
					currentColumn.addBox(currentBox);
					currentReversalPoint = currentBox.bottomPrice - (boxSize * reversalSize);
					newBoxEvent.fire(currentBox);
				}
			}
			else {
				// check if we are above the reversal level. 
				if(price > currentReversalPoint){
					currentColumn = new Column();
					columns.add(currentColumn);
					newColumnEvent.fire(currentColumn);
					while(price>(currentBox.bottomPrice + (1*boxSize))){
						currentBox = new Box();
						currentBox.type = Type.X;
						base = base + boxSize;
						currentBox.bottomPrice = base;
						currentBox.size = boxSize; 
						currentColumn.addBox(currentBox);
						currentReversalPoint = currentBox.bottomPrice - (boxSize * reversalSize);
						newBoxEvent.fire(currentBox);
					}
				}
			}
		}
		else if(price < (currentBox.bottomPrice - (1*boxSize))){
			if(currentBox.type == Type.O){
				while(price<(currentBox.bottomPrice - (1*boxSize))){
					currentBox = new Box();
					currentBox.type = Type.O;
					base = base - boxSize;
					currentBox.bottomPrice = base;
					currentBox.size = boxSize; 
					currentColumn.addBox(currentBox);
					currentReversalPoint = currentBox.bottomPrice + (boxSize * reversalSize);
					newBoxEvent.fire(currentBox);
				}
			}
			else {
				// check if we are above the reversal level. 
				if(price < currentReversalPoint){
					currentColumn = new Column();
					columns.add(currentColumn);
					newColumnEvent.fire(currentColumn);
					while(price<(currentBox.bottomPrice - (1*boxSize))){
						currentBox = new Box();
						currentBox.type = Type.O;
						base = base - boxSize;
						currentBox.bottomPrice = base;
						currentBox.size = boxSize; 
						currentColumn.addBox(currentBox);
						currentReversalPoint = currentBox.bottomPrice + (boxSize * reversalSize);
						newBoxEvent.fire(currentBox);
					}
				}
			}
		}
	}
	/**
	 * returns the associated newColumnEvent(Event&lt;Column&gt;)
	 * @return
	 */
	public Event<Column> getNewColumnEvent() {
		return newColumnEvent;
	}
	/**
	 * sets the associated newColumnEvent(Event&lt;Column&gt;) with the given newColumnEvent(Event&lt;Column&gt;)
	 * @param newColumnEvent
	 */
	public void setNewColumnEvent(Event<Column> newColumnEvent) {
		this.newColumnEvent = newColumnEvent;
	}
	/**
	 * returns the associated newBoxEvent(Event&lt;Box&gt;)
	 * @return
	 */
	public Event<Box> getNewBoxEvent() {
		return newBoxEvent;
	}
	/**
	 * sets the associated newBoxEvent(Event&lt;Box&gt;) with the given newBoxEvent(Event&lt;Box&gt;)
	 * @param newBoxEvent
	 */
	public void setNewBoxEvent(Event<Box> newBoxEvent) {
		this.newBoxEvent = newBoxEvent;
	}
	/**
	 * returns the associated columns(List&lt;Column&gt;)
	 * @return
	 */
	public List<Column> getColumns() {
		return columns;
	}
	
	
	
}
